package file

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/sirupsen/logrus"
	"github.com/xuelang-group/suanpan-appconnector/pkg/common"
	"github.com/xuelang-group/suanpan-appconnector/pkg/storage"
)

var watcher *fsnotify.Watcher
var watchedList []string
var watchJobCloseChList []chan struct{}

// use thread safe because watch event will be single tread
var filePutActionDelayJobHash sync.Map

//var filePutActionDelayJobLock sync.RWMutex

const filePutDelayInterval = 500 * time.Millisecond

func init() {
	var err error
	watcher, err = fsnotify.NewWatcher()
	if err != nil {
		common.PanicError(err)
	}

	//filePutActionDelayJobHash = make(map[string]int32)
}

func checkFilePrefixSuffixNeedIgnore(path string) bool {
	res := strings.HasPrefix(path, "core.") || strings.HasSuffix(path, ".pyc") ||
		strings.HasSuffix(path, ".DS_Store") || strings.HasSuffix(path, ".swp") ||
		strings.HasSuffix(path, "_$folder$") || strings.HasSuffix(path, ".minio")
	return res
}

func checkDirPathNeedWatch(path string) bool {
	res := strings.Contains(path, "__pycache__") || strings.Contains(path, "node_modules")
	return !res
}

func watchDir(path string, fi os.FileInfo, err error) error {

	if err != nil {
		return err
	}

	if fi.Mode().IsDir() { // NOTE: 只会忽略特定目录，文件则不会
		if checkDirPathNeedWatch(path) {
			logrus.Debugf("new file is dir, add to watch %s", path)
			watchedList = append(watchedList, path)
			return watcher.Add(path)
		} else {
			logrus.Debugf("ignore dir watch:%s", path)
		}
	}

	return nil
}

func PushFilesToOss(localPath string, ossPath string) error {
	oss := storage.GetOssInstance()
	logrus.Debugf("start uploading from local:%s to oss:%s", localPath, ossPath)

	// Check if localPath is a directory
	fileInfo, err := os.Stat(localPath)
	if err != nil {
		return err
	}

	if fileInfo.IsDir() {
		// If it's a directory, walk through all files and upload them
		return filepath.Walk(localPath, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if !info.IsDir() {
				// Calculate the relative path
				relPath, err := filepath.Rel(localPath, path)
				if err != nil {
					return err
				}
				// Construct the full OSS path
				fullOssPath := filepath.Join(ossPath, relPath)
				// Convert to forward slashes for OSS
				fullOssPath = strings.ReplaceAll(fullOssPath, "\\", "/")

				logrus.Debugf("uploading file:%s to oss:%s", path, fullOssPath)
				err = oss.FPutObject(fullOssPath, path)
				if err != nil {
					logrus.Errorf("upload to oss failed for %s, %s", path, err)
					return err
				}
			}
			return nil
		})
	} else {
		// If it's a single file, upload it directly
		logrus.Debugf("uploading single file:%s to oss:%s", localPath, ossPath)
		err = oss.FPutObject(ossPath, localPath)
		if err != nil {
			logrus.Errorf("upload to oss failed, %s", err)
			return err
		}
	}

	return nil
}

func downloadFileFromOss(ossPath string, localPath string) error {
	oss := storage.GetOssInstance()
	logrus.Debugf("start download file:%s to local:%s", ossPath, localPath)
	log.Println("start download file:", ossPath, "to local:", localPath)

	// Check if localPath ends with / or \ to determine if it's meant to be a directory
	isDir := strings.HasSuffix(localPath, "/") || strings.HasSuffix(localPath, "\\")

	var targetPath string
	if isDir {
		// If localPath is a directory, append the base name from ossPath
		err := os.MkdirAll(localPath, 0755)
		if err != nil {
			return err
		}
		targetPath = filepath.Join(localPath, filepath.Base(ossPath))
	} else {
		// If localPath is a file, ensure its parent directory exists
		err := os.MkdirAll(filepath.Dir(localPath), 0755)
		if err != nil {
			return err
		}
		targetPath = localPath
	}

	err := oss.FGetObject(ossPath, targetPath)
	if err != nil {
		logrus.Errorf("download from oss failed, %s", err)
		return err
	}

	if strings.HasSuffix(targetPath, ".sh") {
		logrus.Debugf("found sh in path:%s, set chmod to 744", targetPath)
		err = os.Chmod(targetPath, 0744)
		if err != nil {
			common.PanicError(err)
		}
	}
	return nil
}

func SyncFromOssStorage(d *[]common.DownloadList) error {
	if *d == nil || len(*d) == 0 {
		return nil
	}
	oss := storage.GetOssInstance()
	for _, itemDownload := range *d {
		if itemDownload.LocalPath == "" || itemDownload.OssPath == "" {
			//if empty just coninue to ignore error
			//logrus.Warnf("parameters Invalid please check, localPath:%s, ossPath:%s",
			//	itemDownload.LocalPath, itemDownload.OssPath)
			continue
		}
		logrus.Infof("start to sync, local:%s, remote:%s", itemDownload.LocalPath, itemDownload.OssPath)

		if itemDownload.IsDir {
			retObj, err := oss.ListObjects(itemDownload.OssPath, true, 1000 /*hard core refactor*/)
			if err != nil {
				return err
			}
			for _, ossObj := range retObj {
				if ossObj.Name != "" {
					objStoreLocalPath := strings.Replace(ossObj.Name, itemDownload.OssPath, itemDownload.LocalPath, -1)
					err := downloadFileFromOss(ossObj.Name, objStoreLocalPath)
					if err != nil {
						//if download failed just warn and ignore
						logrus.Error(err)
						//return err
					}
				}
			}
		} else {
			err := downloadFileFromOss(itemDownload.OssPath, itemDownload.LocalPath)
			if err != nil {
				//if download failed just warn and ignore
				logrus.Error(err)
				//return err
			}
		}
	}
	return nil
}

func RemoveFileWatcher() {
	//close watched job
	for i := range watchJobCloseChList {
		close(watchJobCloseChList[i])
	}
	//clear joblist Ch
	watchJobCloseChList = nil

	//remove watcher
	for _, path := range watchedList {
		watcher.Remove(path)
	}
	//clear watched list
	watchedList = nil
}

func SetFileWatcher(w *[]common.WatchList) {
	if w == nil || len(*w) == 0 {
		return
	}

	oss := storage.GetOssInstance()
	for _, itemUpload := range *w {
		if itemUpload.LocalPath == "" || itemUpload.OssPath == "" {
			continue
		}
		logrus.Infof("Start to handle watch list Job, local:%s, remote:%s", itemUpload.LocalPath, itemUpload.OssPath)

		if _, err := os.Stat(itemUpload.LocalPath); os.IsNotExist(err) {
			logrus.Warnf("failed to watch local dir not exist: %v",
				itemUpload.LocalPath)
			continue
		}

		//no use close() here
		//defer watcher.Close()
		logrus.Debugf("start to watch:%s , remote path: %s",
			itemUpload.LocalPath, itemUpload.OssPath)

		//use filepath to work with -r function for watch
		if err := filepath.Walk(itemUpload.LocalPath, watchDir); err != nil {
			common.PanicError(fmt.Errorf("local path :%s not exist on oss watch path,please check image, err:%w",
				itemUpload.LocalPath, err))
		}

		watchJobCh := make(chan struct{})
		watchJobCloseChList = append(watchJobCloseChList, watchJobCh)
		go func(itemUpload common.WatchList, finishedCh chan struct{}) {
			for {
				select {
				case _, ok := <-finishedCh:
					if !ok {
						//exit the function
						return
					}
				case event := <-watcher.Events:
					op := event.Op

					//TODO:file size check
					if op&fsnotify.Create == fsnotify.Create {
						logrus.Debugf("detect io create %s", event.Name)
						fileInfo, err := os.Stat(event.Name)
						if err == nil {
							//just watch it if new file created
							//if file not exist just ignore when file edited then upload
							err = watchDir(event.Name, fileInfo, nil)
							if err != nil {
								//create file failed cannot ignore
								common.PanicError(err)
							}
							//if fileInfo.IsDir() {
							//	watcher.Add(event.Name)
							//}

							//2023-09-08 heng:when file create and has data just use write logic
							if !fileInfo.Mode().IsDir() && fileInfo.Size() > 0 { // only care about file
								logrus.Debugf("file create and has data, use write logic %s", event.Name)
								op |= fsnotify.Write
							}
						}
					}
					if op&fsnotify.Remove == fsnotify.Remove {
						logrus.Debugf("detect file delete, oss delete %s", event.Name)
						newOssPath := strings.Replace(itemUpload.OssPath, itemUpload.LocalPath, event.Name, -1)
						err := oss.DeleteObject(newOssPath)
						if err, ok := err.(*storage.StorageError); ok {
							//delete file cannot ignore because local file will be master instance remote must be sync
							common.PanicError(fmt.Errorf("oss Error painc, when delete object %w", err))
						}
					}
					if op&fsnotify.Write == fsnotify.Write {
						newOssPath := strings.Replace(itemUpload.OssPath, itemUpload.LocalPath, event.Name, -1)
						if !checkFilePrefixSuffixNeedIgnore(event.Name) {
							// -1 means init state
							// 0 means start
							// > 0 means multiple change since time after period

							filePutActionDelayJobHash.LoadOrStore(newOssPath+event.Name, -1)

							if i, _ := filePutActionDelayJobHash.Load(newOssPath + event.Name); i.(int) < 0 {
								//set start stat
								filePutActionDelayJobHash.Store(newOssPath+event.Name, 0)
								time.AfterFunc(filePutDelayInterval, func() {
									for {
										var err error
										if i, _ := filePutActionDelayJobHash.Load(newOssPath + event.Name); i == 0 {
											logrus.Debugf("detect file change,push to oss %s", event.Name)
											/*bugfix:
											heng:
											because we use delay push oss file
											if the tempfile created, like package-lock.json.abcd1234
											when npm install when delay push action,
											the file was deleted, we need check file before we put */
											if _, err := os.Stat(event.Name); errors.Is(err, os.ErrNotExist) {
												// path/to/whatever does not exist
												logrus.Infof("prepare to fput object to oss,but file:%s no longer exist ", event.Name)
												break
											}

											err = oss.FPutObject(newOssPath, event.Name)
											if err != nil {
												//common.PanicError(fmt.Errorf("oss Error painc %s", err.Error()))
												/*just don't panic error when fput object failed just like:
												  - package-lock.json.124124 file
												  - io lock file
												  - other file */

												//2023-4-3
												//requirement from weichai
												//just retry fputoject when error ONCE!
												err = oss.FPutObject(newOssPath, event.Name)
												if err != nil {
													logrus.Errorf("error when oss fputObject, %v", err)
												}
												break
											}
											//set to init stat
											filePutActionDelayJobHash.Store(newOssPath+event.Name, -1)
											logrus.Debugf("oss file:%s push complete, remote path:%s", event.Name, newOssPath)
											break
										} else {
											filePutActionDelayJobHash.Store(newOssPath+event.Name, 0)
										}
										//sleep interval
										time.Sleep(filePutDelayInterval)
									}
								})
							} else {
								// maybe we need lock here?
								// or only miss one push sequence?
								if val, ok := filePutActionDelayJobHash.Load(newOssPath + event.Name); ok {
									filePutActionDelayJobHash.Store(newOssPath+event.Name, val.(int)+1)
								}
							}

						} else {
							logrus.Debugf("ingore path: %s", event.Name)
						}
					}
					if op&fsnotify.Rename == fsnotify.Rename {
						logrus.Debugf("detect rename event,delete old, oss delete %s", event.Name)
						newOssPath := strings.Replace(itemUpload.OssPath, itemUpload.LocalPath, event.Name, -1)
						err := oss.DeleteObject(newOssPath)
						if err, ok := err.(*storage.StorageError); ok {
							//delete file cannot ignore because local file will be master instance remote must be sync
							common.PanicError(fmt.Errorf("oss Error painc %w", err))
						}
					}
					if op&fsnotify.Chmod == fsnotify.Chmod {
						//ignore
						_ = 3
					}
				case err := <-watcher.Errors:
					logrus.Error("Error on file watcher", err)
				}
			}
		}(itemUpload, watchJobCh)
	}

}
